﻿// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable CommentTypo
// ReSharper disable CompareOfFloatsByEqualityOperator
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable StringLiteralTypo
// ReSharper disable UnusedParameter.Local

/* TextShaper.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using HarfBuzzSharp;

using SkiaSharp;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;

using AM.Skia.RichTextKit.Utils;

#endregion

#nullable enable

namespace AM.Skia.RichTextKit;

/// <summary>
/// Helper class for shaping text
/// </summary>
internal class TextShaper
    : IDisposable
{
    /// <summary>
    /// Cache of shapers for typefaces
    /// </summary>
    private static Dictionary<SKTypeface, TextShaper> _shapers = new ();

    /// <summary>
    /// Get the text shaper for a particular type face
    /// </summary>
    /// <param name="typeface">The typeface being queried for</param>
    /// <returns>A TextShaper</returns>
    public static TextShaper ForTypeface (SKTypeface typeface)
    {
        lock (_shapers)
        {
            if (!_shapers.TryGetValue (typeface, out var shaper))
            {
                shaper = new TextShaper (typeface);
                _shapers.Add (typeface, shaper);
            }

            return shaper;
        }
    }

    /// <summary>
    /// Constructs a new TextShaper
    /// </summary>
    /// <param name="typeface">The typeface of this shaper</param>
    private TextShaper (SKTypeface typeface)
    {
        // Store typeface
        _typeface = typeface;

        // Load the typeface stream to a HarfBuzz font
        int index;
        using (var blob = GetHarfBuzzBlob (typeface.OpenStream (out index)))
        using (var face = new Face (blob, (uint)index))
        {
            face.UnitsPerEm = typeface.UnitsPerEm;

            _font = new Font (face);
            _font.SetScale (overScale, overScale);
            _font.SetFunctionsOpenType();
        }

        // Get font metrics for this typeface
        using (var paint = new SKPaint())
        {
            paint.Typeface = typeface;
            paint.TextSize = overScale;
            _fontMetrics = paint.FontMetrics;

            // This is a temporary hack until SkiaSharp exposes
            // a way to check if a font is fixed pitch.  For now
            // we just measure and `i` and a `w` and see if they're
            // the same width.
            var widths = paint.GetGlyphWidths ("iw", out _);
            _isFixedPitch = widths != null && widths.Length > 1 && widths[0] == widths[1];
            if (_isFixedPitch)
            {
                _fixedCharacterWidth = widths![0];
            }
        }
    }

    /// <summary>
    /// Dispose this text shaper
    /// </summary>
    public void Dispose()
    {
        if (_font != null)
        {
            _font.Dispose();
            _font = null;
        }
    }

    /// <summary>
    /// The HarfBuzz font for this shaper
    /// </summary>
    private Font? _font;

    /// <summary>
    /// The typeface for this shaper
    /// </summary>
    private SKTypeface _typeface;

    /// <summary>
    /// Font metrics for the font
    /// </summary>
    private SKFontMetrics _fontMetrics;

    /// <summary>
    /// True if this font face is fixed pitch
    /// </summary>
    private bool _isFixedPitch;

    /// <summary>
    /// Fixed pitch character width
    /// </summary>
    private float _fixedCharacterWidth;

    /// <summary>
    /// A set of re-usable result buffers to store the result of text shaping operation
    /// </summary>
    public class ResultBufferSet
    {
        public void Clear()
        {
            GlyphIndicies.Clear();
            GlyphPositions.Clear();
            Clusters.Clear();
            CodePointXCoords.Clear();
        }

        public Buffer<ushort> GlyphIndicies = new ();
        public Buffer<SKPoint> GlyphPositions = new ();
        public Buffer<int> Clusters = new ();
        public Buffer<float> CodePointXCoords = new ();
    }

    /// <summary>
    /// Returned as the result of a text shaping operation
    /// </summary>
    public struct Result
    {
        /// <summary>
        /// The glyph indicies of all glyphs required to render the shaped text
        /// </summary>
        public Slice<ushort> GlyphIndicies;

        /// <summary>
        /// The position of each glyph
        /// </summary>
        public Slice<SKPoint> GlyphPositions;

        /// <summary>
        /// One entry for each glyph, showing the code point index
        /// of the characters it was derived from
        /// </summary>
        public Slice<int> Clusters;

        /// <summary>
        /// The end position of the rendered text
        /// </summary>
        public SKPoint EndXCoord;

        /// <summary>
        /// The X-Position of each passed code point
        /// </summary>
        public Slice<float> CodePointXCoords;

        /// <summary>
        /// The ascent of the font
        /// </summary>
        public float Ascent;

        /// <summary>
        /// The descent of the font
        /// </summary>
        public float Descent;

        /// <summary>
        /// The leading of the font
        /// </summary>
        public float Leading;

        /// <summary>
        /// The XMin for the font
        /// </summary>
        public float XMin;
    }

    /// <summary>
    /// Over scale used for all font operations
    /// </summary>
    private const int overScale = 512;


    /// <summary>
    /// Shape an array of utf-32 code points replacing each grapheme cluster with a replacement character
    /// </summary>
    /// <param name="bufferSet">A re-usable text shaping buffer set that results will be allocated from</param>
    /// <param name="codePoints">The utf-32 code points to be shaped</param>
    /// <param name="style">The user style for the text</param>
    /// <param name="clusterAdjustment">A value to add to all reported cluster numbers</param>
    /// <returns>A TextShaper.Result representing the shaped text</returns>
    public Result ShapeReplacement (ResultBufferSet bufferSet, Slice<int> codePoints, IStyle style,
        int clusterAdjustment)
    {
        var clusters = GraphemeClusterAlgorithm.GetBoundaries (codePoints).ToArray();
        var glyph = _typeface.GetGlyph (style.ReplacementCharacter);
        var font = new SKFont (_typeface, overScale);
        var glyphScale = style.FontSize / overScale;

        var widths = new float[1];
        var bounds = new SKRect[1];
        font.GetGlyphWidths (new[] { glyph }.AsSpan(), widths.AsSpan(), bounds.AsSpan());

        var r = new Result();
        r.GlyphIndicies = bufferSet.GlyphIndicies.Add (clusters.Length - 1, false);
        r.GlyphPositions = bufferSet.GlyphPositions.Add (clusters.Length - 1, false);
        r.Clusters = bufferSet.Clusters.Add (clusters.Length - 1, false);
        r.CodePointXCoords = bufferSet.CodePointXCoords.Add (codePoints.Length, false);
        r.CodePointXCoords.Fill (0);

        float xCoord = 0;
        for (var i = 0; i < clusters.Length - 1; i++)
        {
            r.GlyphPositions[i].X = xCoord * glyphScale;
            r.GlyphPositions[i].Y = 0;
            r.GlyphIndicies[i] = codePoints[clusters[i]] == 0x2029 ? (ushort)0 : glyph;
            r.Clusters[i] = clusters[i] + clusterAdjustment;

            for (var j = clusters[i]; j < clusters[i + 1]; j++)
            {
                r.CodePointXCoords[j] = r.GlyphPositions[i].X;
            }

            xCoord += widths[0] + style.LetterSpacing / glyphScale;
        }

        // Also return the end cursor position
        r.EndXCoord = new SKPoint (xCoord * glyphScale, 0);

        ApplyFontMetrics (ref r, style.FontSize);

        return r;
    }


    /// <summary>
    /// Shape an array of utf-32 code points
    /// </summary>
    /// <param name="bufferSet">A re-usable text shaping buffer set that results will be allocated from</param>
    /// <param name="codePoints">The utf-32 code points to be shaped</param>
    /// <param name="style">The user style for the text</param>
    /// <param name="direction">LTR or RTL direction</param>
    /// <param name="clusterAdjustment">A value to add to all reported cluster numbers</param>
    /// <param name="asFallbackFor">The type face this font is a fallback for</param>
    /// <param name="textAlignment">The text alignment of the paragraph, used to control placement of glyphs within character cell when letter spacing used</param>
    /// <returns>A TextShaper.Result representing the shaped text</returns>
    public Result Shape (ResultBufferSet bufferSet, Slice<int> codePoints, IStyle style, TextDirection direction,
        int clusterAdjustment, SKTypeface asFallbackFor, TextAlignment textAlignment)
    {
        // Work out if we need to force this to a fixed pitch and if
        // so the unscale character width we need to use
        float forceFixedPitchWidth = 0;
        if (asFallbackFor != _typeface && asFallbackFor != null!)
        {
            var originalTypefaceShaper = ForTypeface (asFallbackFor);
            if (originalTypefaceShaper._isFixedPitch)
            {
                forceFixedPitchWidth = originalTypefaceShaper._fixedCharacterWidth;
            }
        }

        // Work out how much to shift glyphs in the character cell when using letter spacing
        // The idea here is to align the glyphs within the character cell the same way as the
        // text block alignment so that left/right aligned text still aligns with the margin
        // and centered text is still centered (and not shifted slightly due to the extra
        // space that would be at the right with normal letter spacing).
        float glyphLetterSpacingAdjustment = 0;
        switch (textAlignment)
        {
            case TextAlignment.Right:
                glyphLetterSpacingAdjustment = style.LetterSpacing;
                break;

            case TextAlignment.Center:
                glyphLetterSpacingAdjustment = style.LetterSpacing / 2;
                break;
        }


        using (var buffer = new HarfBuzzSharp.Buffer())
        {
            // Setup buffer
            buffer.AddUtf32 (codePoints.AsSpan(), 0, -1);

            // Setup directionality (if supplied)
            switch (direction)
            {
                case TextDirection.LTR:
                    buffer.Direction = Direction.LeftToRight;
                    break;

                case TextDirection.RTL:
                    buffer.Direction = Direction.RightToLeft;
                    break;

                default:
                    throw new ArgumentException (nameof (direction));
            }

            // Guess other attributes
            buffer.GuessSegmentProperties();

            // Shape it
            _font!.Shape (buffer);

            // RTL?
            var rtl = buffer.Direction == Direction.RightToLeft;

            // Work out glyph scaling and offsetting for super/subscript
            var glyphScale = style.FontSize / overScale;
            float glyphVOffset = 0;
            if (style.FontVariant == FontVariant.SuperScript)
            {
                glyphScale *= 0.65f;
                glyphVOffset -= style.FontSize * 0.35f;
            }

            if (style.FontVariant == FontVariant.SubScript)
            {
                glyphScale *= 0.65f;
                glyphVOffset += style.FontSize * 0.1f;
            }

            // Create results and get buffes
            var r = new Result();
            r.GlyphIndicies = bufferSet.GlyphIndicies.Add (buffer.Length, false);
            r.GlyphPositions = bufferSet.GlyphPositions.Add (buffer.Length, false);
            r.Clusters = bufferSet.Clusters.Add (buffer.Length, false);
            r.CodePointXCoords = bufferSet.CodePointXCoords.Add (codePoints.Length, false);
            r.CodePointXCoords.Fill (0);

            // Convert points
            var gp = buffer.GlyphPositions;
            var gi = buffer.GlyphInfos;
            float cursorX = 0;
            float cursorY = 0;
            float cursorXCluster = 0;
            for (var i = 0; i < buffer.Length; i++)
            {
                r.GlyphIndicies[i] = (ushort)gi[i].Codepoint;
                r.Clusters[i] = (int)gi[i].Cluster + clusterAdjustment;


                // Update code point positions
                if (!rtl)
                {
                    // First cluster, different cluster, or same cluster with lower x-coord
                    if (i == 0 ||
                        r.Clusters[i] != r.Clusters[i - 1] ||
                        cursorX < r.CodePointXCoords[r.Clusters[i] - clusterAdjustment])
                    {
                        r.CodePointXCoords[r.Clusters[i] - clusterAdjustment] = cursorX;
                    }
                }

                // Get the position
                var pos = gp[i];

                // Update glyph position
                r.GlyphPositions[i] = new SKPoint (
                        cursorX + pos.XOffset * glyphScale + glyphLetterSpacingAdjustment,
                        cursorY - pos.YOffset * glyphScale + glyphVOffset
                    );

                // Update cursor position
                cursorX += pos.XAdvance * glyphScale;
                cursorY += pos.YAdvance * glyphScale;

                // Ensure paragraph separator character (0x2029) has some
                // width so it can be seen as part of the selection in the editor.
                if (pos.XAdvance == 0 && codePoints[(int)gi[i].Cluster] == 0x2029)
                {
                    cursorX += style.FontSize * 2 / 3;
                }

                if (i + 1 == gi.Length || gi[i].Cluster != gi[i + 1].Cluster)
                {
                    cursorX += style.LetterSpacing;
                }

                // Are we falling back for a fixed pitch font and is the next character a
                // new cluster?  If so advance by the width of the original font, not this
                // fallback font
                if (forceFixedPitchWidth != 0)
                {
                    // New cluster?
                    if (i + 1 >= buffer.Length || gi[i].Cluster != gi[i + 1].Cluster)
                    {
                        // Work out fixed pitch position of next cluster
                        cursorXCluster += forceFixedPitchWidth * glyphScale;
                        if (cursorXCluster > cursorX)
                        {
                            // Nudge characters to center them in the fixed pitch width
                            if (i == 0 || gi[i - 1].Cluster != gi[i].Cluster)
                            {
                                r.GlyphPositions[i].X += (cursorXCluster - cursorX) / 2;
                            }

                            // Use fixed width character position
                            cursorX = cursorXCluster;
                        }
                        else
                        {
                            // Character is wider (probably an emoji) so we
                            // allow it to exceed the fixed pitch character width
                            cursorXCluster = cursorX;
                        }
                    }
                }

                // Store RTL cursor position
                if (rtl)
                {
                    // First cluster, different cluster, or same cluster with lower x-coord
                    if (i == 0 ||
                        r.Clusters[i] != r.Clusters[i - 1] ||
                        cursorX > r.CodePointXCoords[r.Clusters[i] - clusterAdjustment])
                    {
                        r.CodePointXCoords[r.Clusters[i] - clusterAdjustment] = cursorX;
                    }
                }
            }

            // Finalize cursor positions by filling in any that weren't
            // referenced by a cluster
            if (rtl)
            {
                r.CodePointXCoords[0] = cursorX;
                for (var i = codePoints.Length - 2; i >= 0; i--)
                {
                    if (r.CodePointXCoords[i] == 0)
                    {
                        r.CodePointXCoords[i] = r.CodePointXCoords[i + 1];
                    }
                }
            }
            else
            {
                for (var i = 1; i < codePoints.Length; i++)
                {
                    if (r.CodePointXCoords[i] == 0)
                    {
                        r.CodePointXCoords[i] = r.CodePointXCoords[i - 1];
                    }
                }
            }

            // Also return the end cursor position
            r.EndXCoord = new SKPoint (cursorX, cursorY);

            // And some other useful metrics
            ApplyFontMetrics (ref r, style.FontSize);

            // Done
            return r;
        }
    }

    private void ApplyFontMetrics (ref Result result, float fontSize)
    {
        // And some other useful metrics
        result.Ascent = _fontMetrics.Ascent * fontSize / overScale;
        result.Descent = _fontMetrics.Descent * fontSize / overScale;
        result.Leading = _fontMetrics.Leading * fontSize / overScale;
        result.XMin = _fontMetrics.XMin * fontSize / overScale;
    }

    private static Blob GetHarfBuzzBlob (SKStreamAsset asset)
    {
        if (asset == null)
        {
            throw new ArgumentNullException (nameof (asset));
        }

        Blob blob;

        var size = asset.Length;
        var memoryBase = asset.GetMemoryBase();
        if (memoryBase != IntPtr.Zero)
        {
            // the underlying stream is really a mamory block
            // so save on copying and just use that directly
            blob = new Blob (memoryBase, size, MemoryMode.ReadOnly, () => asset.Dispose());
        }
        else
        {
            // this could be a forward-only stream, so we must copy
            var ptr = Marshal.AllocCoTaskMem (size);
            asset.Read (ptr, size);
            blob = new Blob (ptr, size, MemoryMode.ReadOnly, () => Marshal.FreeCoTaskMem (ptr));
        }

        // make immutable for performance?
        blob.MakeImmutable();

        return blob;
    }
}
